Skip to content

SCIX-847 Handle blocked browser storage gracefully#841

Draft
thostetler wants to merge 7 commits intoadsabs:masterfrom
thostetler:scix-847-browser-storage
Draft

SCIX-847 Handle blocked browser storage gracefully#841
thostetler wants to merge 7 commits intoadsabs:masterfrom
thostetler:scix-847-browser-storage

Conversation

@thostetler
Copy link
Copy Markdown
Member

When a user has cookies or site data blocked in their browser, the app crashes with a SecurityError thrown by localStorage access during Zustand store initialization. Iron-session also cannot persist session cookies, causing continuous re-bootstrapping on every page load.

  • Add browserStorage utility module with safe wrappers (try/catch) for localStorage and sessionStorage, and availability probes for cookies and localStorage
  • Update Zustand persist middleware to use the safe storage adapter via getStorage option, preventing the crash
  • Replace direct localStorage/sessionStorage access in useLandingFormPreference, SiteAlert, and useScrollRestoration with safe wrappers
  • Render a full-page blocking notice when cookies are unavailable (app cannot maintain a stable session)
  • Render a dismissable warning banner when localStorage is blocked but cookies work (session is stable, but preferences won't persist)

Copilot AI review requested due to automatic review settings April 9, 2026 18:31
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 9, 2026

Codecov Report

❌ Patch coverage is 84.43396% with 33 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.6%. Comparing base (006cf95) to head (54858be).

Files with missing lines Patch % Lines
src/lib/browserStorage.ts 78.7% 29 Missing ⚠️
src/lib/useLandingFormPreference.ts 80.0% 3 Missing ⚠️
src/store/store.ts 50.0% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##           master    #841     +/-   ##
========================================
+ Coverage    62.5%   62.6%   +0.2%     
========================================
  Files         317     320      +3     
  Lines       36576   36765    +189     
  Branches     1673    1691     +18     
========================================
+ Hits        22827   22984    +157     
- Misses      13709   13741     +32     
  Partials       40      40             
Files with missing lines Coverage Δ
...ts/StorageDegradedBanner/StorageDegradedBanner.tsx 100.0% <100.0%> (ø)
...rageUnavailableNotice/StorageUnavailableNotice.tsx 100.0% <100.0%> (ø)
src/store/store.ts 60.4% <50.0%> (-0.1%) ⬇️
src/lib/useLandingFormPreference.ts 94.1% <80.0%> (+0.5%) ⬆️
src/lib/browserStorage.ts 78.7% <78.7%> (ø)

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds defensive browser storage handling so the app doesn’t crash when localStorage/sessionStorage access throws SecurityError, and introduces UX fallbacks for blocked cookies/storage (moderate regression risk due to app-wide bootstrapping changes and new SSR/client branching).

Changes:

  • Introduces browserStorage utilities (safe local/session storage wrappers + availability probes) and wires Zustand persist to use a safe storage adapter.
  • Replaces direct localStorage/sessionStorage usage in a few hooks/components with safe wrappers.
  • Adds UI: a full-page “cookies required” notice and a dismissible degraded-storage banner, plus tests for the new utilities/components.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/store/store.ts Switches Zustand persist middleware to use createSafeStorage to avoid crashes when storage access throws.
src/pages/_app.tsx Adds cookie/localStorage availability checks to render a blocking notice or a degraded banner on the client.
src/lib/useScrollRestoration.ts Replaces sessionStorage direct calls with safe wrapper functions.
src/lib/useLandingFormPreference.ts Replaces localStorage direct calls with safe wrapper functions.
src/lib/browserStorage.ts New module providing safe storage wrappers and availability probes, plus a Zustand StateStorage adapter.
src/lib/tests/browserStorage.test.ts Adds Vitest coverage for the new storage utilities.
src/components/StorageUnavailableNotice/StorageUnavailableNotice.tsx New full-page notice UI shown when cookies are unavailable.
src/components/StorageUnavailableNotice/StorageUnavailableNotice.test.tsx Adds tests for the notice component.
src/components/StorageDegradedBanner/StorageDegradedBanner.tsx New dismissible warning banner for blocked localStorage (cookies still work).
src/components/StorageDegradedBanner/StorageDegradedBanner.test.tsx Adds tests for the banner component.
src/components/SiteAlert/SiteAlert.tsx Replaces localStorage direct calls with safe wrapper functions for dismiss state.

Comment on lines +8 to +12
Browser storage is required
</Heading>
<Text>
SciX uses cookies and browser storage to maintain your session and preferences. Both are currently blocked by
your browser settings.
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The notice copy says "cookies and browser storage ... Both are currently blocked", but this component is rendered based on cookies being unavailable (localStorage may still work). This message is misleading; adjust wording to focus on cookies/site data being blocked (and only mention local storage if you actually detect it).

Suggested change
Browser storage is required
</Heading>
<Text>
SciX uses cookies and browser storage to maintain your session and preferences. Both are currently blocked by
your browser settings.
Cookies are required
</Heading>
<Text>
SciX uses cookies and site data to maintain your session and preferences. Cookies appear to be blocked by your
browser settings.

Copilot uses AI. Check for mistakes.
Comment thread src/lib/browserStorage.ts
Comment on lines +52 to +59
export function createSafeStorage(): StateStorage {
return {
getItem(name: string): string | null {
try {
return localStorage.getItem(name);
} catch {
logger.debug({ name }, 'browserStorage: getItem failed');
return null;
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createSafeStorage() will also be used during SSR (the store is created server-side). In SSR, referencing localStorage will throw a ReferenceError that gets caught, but it will log debug messages and incur try/catch overhead on every request. Consider short-circuiting when window is undefined to return a no-op storage adapter without logging.

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +6
import { StorageUnavailableNotice } from './StorageUnavailableNotice';

const renderWithChakra = (ui: React.ReactElement) => render(<ChakraProvider>{ui}</ChakraProvider>);
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test uses the React namespace type (React.ReactElement) without importing React, which will fail under tsconfig jsx=react-jsx ("Cannot find namespace 'React'"). Import type ReactElement from 'react' (or remove the annotation) and use that instead.

Suggested change
import { StorageUnavailableNotice } from './StorageUnavailableNotice';
const renderWithChakra = (ui: React.ReactElement) => render(<ChakraProvider>{ui}</ChakraProvider>);
import type { ReactElement } from 'react';
import { StorageUnavailableNotice } from './StorageUnavailableNotice';
const renderWithChakra = (ui: ReactElement) => render(<ChakraProvider>{ui}</ChakraProvider>);

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +7
import { ChakraProvider } from '@chakra-ui/react';
import { StorageDegradedBanner } from './StorageDegradedBanner';

const renderWithChakra = (ui: React.ReactElement) => render(<ChakraProvider>{ui}</ChakraProvider>);
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test uses the React namespace type (React.ReactElement) without importing React, which will fail under tsconfig jsx=react-jsx ("Cannot find namespace 'React'"). Import type ReactElement from 'react' (or remove the annotation) and use that instead.

Suggested change
import { ChakraProvider } from '@chakra-ui/react';
import { StorageDegradedBanner } from './StorageDegradedBanner';
const renderWithChakra = (ui: React.ReactElement) => render(<ChakraProvider>{ui}</ChakraProvider>);
import type { ReactElement } from 'react';
import { ChakraProvider } from '@chakra-ui/react';
import { StorageDegradedBanner } from './StorageDegradedBanner';
const renderWithChakra = (ui: ReactElement) => render(<ChakraProvider>{ui}</ChakraProvider>);

Copilot uses AI. Check for mistakes.
@thostetler thostetler force-pushed the scix-847-browser-storage branch 2 times, most recently from b4f9f3f to bd5b08e Compare April 9, 2026 19:37
…sist

Introduces browserStorage.ts with:
- isCookiesAvailable/isLocalStorageAvailable probes (SecurityError-safe)
- createSafeStorage: Zustand StateStorage adapter with SSR no-op short-circuit
- safeLocalStorage*/safeSessionStorage* wrappers for direct call sites

Wires createSafeStorage into Zustand persist middleware so store hydration
no longer throws when localStorage access is denied.
Replaces localStorage/sessionStorage direct calls throughout with safe
wrappers from browserStorage.ts. Prevents SecurityError from surfacing as
uncaught or render-phase errors when browser blocks site data access.

Files updated: useLandingFormPreference, useScrollRestoration, SiteAlert,
useTour, pages/index, pages/search/index, pages/abs/abstract.

Also moves useLandingFormPreference's initial read from a useState lazy
initializer to useEffect, preventing render-phase SecurityError under
Next.js/Turbopack.
- StorageUnavailableNotice: full-page blocking notice when cookies are unavailable,
  with links to browser-specific enable instructions
- StorageDegradedBanner: dismissible warning banner when localStorage is blocked
  but cookies still work
- _app.tsx: probes storage once on mount via useState+useEffect, renders the
  appropriate notice or banner without SSR hydration mismatch
@thostetler thostetler force-pushed the scix-847-browser-storage branch from bd5b08e to f1b550f Compare April 9, 2026 20:52
Adds a banner prop to Layout so StorageDegradedBanner renders at full
width between the navbar and main content, rather than inside the
Container which constrains it with horizontal padding.
When localStorage throws, safeLocalStorageGet/Set now read/write a
module-level Map instead of silently no-oping. This keeps dismissal state
(e.g. tour 'seen' flags) alive for the page session even with storage blocked,
preventing the tour from re-triggering on every render.
@thostetler thostetler marked this pull request as draft April 9, 2026 21:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants